#! /usr/bin/python3
#
# The original enum-converter.py may be found at:
# https://github.com/PackageKit/PackageKit/blob/b64ee9dfa707d5dd2b93c8eebe9930a55fcde108/lib/python/enum-convertor.py
#
# Copyright (C) 2008 - 2012 PackageKit Authors
#
# Licensed under the GNU General Public License Version 2.0
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# The following modifications have been made to the original
# script:
#  * Print the time of conversion
#  * Extract and print the original license of the source
#  * Enumerations are extracted from the header file to preserve
#    order
#  * Use Python "Flag" Enumerations
#  * Extract comments and include them as docstrings
#  * Introduce a string constant validation mode.  This extracts
#    strings from pk-enum.c, then compares to the calculated
#    strings from pk-enum.h.
#
# Copyright (C) 2022 Eric Callahan <arksine.code@gmail.com>
#
# Usage:
#     pk-enum-converter.py pk_enum.h > enums.py
#
# Enum String Validation Mode:
#     pk-enum-converter.py pk_enum.h pk_enum.c
#
# The pk_enum source files, pk-enum.c and pk-enum.h, can be found in the
# PackageKit GitHub repo:
# https://github.com/PackageKit/PackageKit/blob/main/lib/packagekit-glib2/pk-enum.c
# https://github.com/PackageKit/PackageKit/blob/main/lib/packagekit-glib2/pk-enum.h
#
from __future__ import print_function

from re import compile, DOTALL, MULTILINE
import time
import sys
import pathlib
import textwrap

HEADER = \
'''
# This file was autogenerated from %s by pk-enum-converter.py
# on %s UTC
#
# License for original source:
#
%s

from __future__ import annotations
import sys
from enum import Flag, auto

class PkFlag(Flag):
    @classmethod
    def from_pkstring(cls, pkstring: str):
        for name, member in cls.__members__.items():
            if member.pkstring == pkstring:
                return cls(member.value)
        # Return "unknown" flag
        return cls(1)

    @classmethod
    def from_index(cls, index: int):
        return cls(1 << index)

    @property
    def pkstring(self) -> str:
        if self.name is None:
            return " | ".join([f.pkstring for f in self])
        return self.name.lower().replace("_", "-")

    @property
    def desc(self) -> str:
        if self.name is None:
            return ", ".join([f.desc for f in self])
        description = self.name.lower().replace("_", " ")
        return description.capitalize()

    @property
    def index(self) -> int:
        return self.value.bit_length() - 1

    if sys.version_info < (3, 11):
        def __iter__(self):
            for i in range(self._value_.bit_length()):
                val = 1 << i
                if val & self._value_ == val:
                    yield self.__class__(val)
'''  # noqa: E122

FILTER_PKSTRING = \
'''    @property
    def pkstring(self) -> str:
        pks = self.name
        if pks is None:
            return " | ".join([f.pkstring for f in self])
        if pks in ["DEVELOPMENT", "NOT_DEVELOPMENT"]:
            pks = pks[:-6]
        if pks[:4] == "NOT_":
            pks = "~" + pks[4:]
        return pks.lower().replace("_", "-")
'''  # noqa: E122

ERROR_PROPS = \
'''    @property
    def pkstring(self) -> str:
        if self == Error.UPDATE_FAILED_DUE_TO_RUNNING_PROCESS:
            return "failed-due-to-running-process"
        return super().pkstring
'''  # noqa: E122

ALIASES = {
    "Error.OOM": "OUT_OF_MEMORY"
}

header_enum = compile(r"/\*\*\n(.+?)@PK_[A-Z_]+_ENUM_LAST:\s+\*\s+(.+?)"
                      r"\s+\*\*/\s+typedef enum {(.+?)} Pk(.+?)Enum",
                      DOTALL | MULTILINE)
header_value = compile(r"(PK_[A-Z_]+_ENUM)_([A-Z0-9_]+)")
header_desc = compile(r"@PK_[A-Z_]+_ENUM_([A-Z_]+):(.*)")
license = compile(r"(Copyright.+?)\*/", DOTALL | MULTILINE)
enum_h_name = sys.argv[1]
header = pathlib.Path(enum_h_name).read_text()

# Get License
lic_match = license.search(header)
assert lic_match is not None
lic_parts = lic_match.group(1).split("\n")
lic = "\n".join([("# " + p.lstrip("* ")).rstrip(" ") for p in lic_parts])


if len(sys.argv) > 2:
    # Validation Mode, extract strings from the source file, compare to
    # those calculated from the enums in the header file
    enum_to_string = {}
    enum = compile(r"static const PkEnumMatch enum_([^\]]+)\[\] = {(.*?)};",
                   DOTALL | MULTILINE)
    value = compile(r"(PK_[A-Z_]+_ENUM_[A-Z0-9_]+),\s+\"([^\"]+)\"")
    enum_c_name = sys.argv[2]
    inp = pathlib.Path(enum_c_name).read_text()
    for (name, data) in enum.findall(inp):
        for (enum_name, string) in value.findall(data):
            enum_to_string[enum_name] = string
    for (desc_data, comments, data, name) in header_enum.findall(header):
        for (prefix, short_name) in header_value.findall(data):
            if short_name == "LAST":
                continue
            # Validation Mode
            enum_name = f"{prefix}_{short_name}"
            string = enum_to_string[enum_name]
            calc_string = short_name.lower().replace("_", "-")
            if calc_string[:4] == "not-" and name == "Filter":
                calc_string = "~" + calc_string[4:]
            if calc_string != string:
                print(
                    f"Calculated String Mismatch: {name}.{short_name}\n"
                    f"Calculated: {calc_string}\n"
                    f"Extracted: {string}\n")
    exit(0)

print(HEADER % (enum_h_name, time.asctime(time.gmtime()), lic))
# Use the header file for correct enum ordering
for (desc_data, comments, data, name) in header_enum.findall(header):

    print(f"\nclass {name}(PkFlag):")
    # Print Docstring
    print('    """')
    comments = [(" " * 4 + c.lstrip("* ")).rstrip(" ")
                for c in comments.splitlines()]
    for comment in comments:
        comment = comment.expandtabs(4)
        if len(comment) > 79:
            comment = "\n".join(textwrap.wrap(
                comment, 79, subsequent_indent="    ",
                tabsize=4))
        print(comment)
    print("")
    for (item, desc) in header_desc.findall(desc_data):
        line = f"    * {name}.{item}:  {desc.strip()}".rstrip()
        if len(line) > 88:
            print(f"    * {name}.{item}:")
            print(f"        {desc}")
        else:
            print(line)
    print('    """')
    if name == "Filter":
        print(FILTER_PKSTRING)
    elif name == "Error":
        print(ERROR_PROPS)
    aliases = []
    for (prefix, short_name) in header_value.findall(data):
        if short_name == "LAST":
            continue
        long_name = f"{name}.{short_name}"
        if long_name in ALIASES:
            alias = ALIASES[long_name]
            aliases.append((short_name, alias))
            short_name = alias
        # Print Enums
        print(f"    {short_name} = auto()")
    for name, alias in aliases:
        print(f"    {name} = {alias}")
